/**
 * 
 */
package cyclopean.world.zone;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javagame.core.main.Game;
import com.sun.opengl.util.BufferUtil;
import javax.imageio.ImageIO;
import javax.media.opengl.GL;

import org.apache.log4j.Logger;

import cyclopean.world.zone.Zone.ZoneListener;

// TODO Javadoc
/**
 * 
 *
 * @author Jaco van der Westhuizen
 */
public class ZoneRenderer implements ZoneListener
{
	private static final Logger logger = Logger.getLogger(ZoneRenderer.class);

	static final int[] glTex = new int[] {0};
	static final int TEX_DETAIL = 16;

	static boolean initStatic = false;

	private static final float TEX_X_STEP = 1.0f / BlockType.X_COUNT;
	private static final float TEX_Y_STEP = 1.0f / BlockType.Y_COUNT;
	private static final int BYTES = Float.SIZE / 8;
	
	private static void init()
	{
		GL gl = Game.get().grabGL();

		gl.glEnable(GL.GL_TEXTURE_2D);
		gl.glEnable(GL.GL_CULL_FACE);
		gl.glEnable(GL.GL_BLEND);
		gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA);
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glDepthFunc(GL.GL_LEQUAL);

		// Generate texture atlas for block textures
		final int sizeX = TEX_DETAIL * BlockType.X_COUNT * 2;
		final int sizeY = TEX_DETAIL * BlockType.Y_COUNT * 2;
		
		gl.glGenTextures(1, glTex, 0);

		gl.glBindTexture(GL.GL_TEXTURE_2D, glTex[0]);
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT);
		gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT);
		gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA8, sizeX, sizeY, 0, GL.GL_BGRA, GL.GL_UNSIGNED_BYTE, null);

		Game.get().releaseGL();
		
		initStatic = true;
	}

	public static void addBlockType(byte id, String fileName)
	{
		if (!initStatic)
			init();
		
		final File file = new File(fileName);
		BufferedImage img = null;

		try
		{
			img = ImageIO.read(file);
		}
		catch (IOException x)
		{
			logger.error("Failed to load image for block number " + id + ", '" + file.getName() + "'");
			return;
		}

		final int[] rgba = new int[TEX_DETAIL * TEX_DETAIL * 4];
		int x = (id % BlockType.X_COUNT) * TEX_DETAIL * 2;
		int y = (id / BlockType.X_COUNT) * TEX_DETAIL * 2;

		img.getRGB(0, 0, TEX_DETAIL * 2, TEX_DETAIL * 2, rgba, 0, 32);

		IntBuffer buffer = BufferUtil.newIntBuffer(rgba.length);
		buffer.put(rgba);
		buffer.rewind();
		
        GL gl = Game.get().grabGL();

        gl.glBindTexture(GL.GL_TEXTURE_2D, glTex[0]);
		gl.glTexSubImage2D(GL.GL_TEXTURE_2D, 0, x, y, TEX_DETAIL * 2, TEX_DETAIL * 2, GL.GL_BGRA, GL.GL_UNSIGNED_BYTE, buffer);

		Game.get().releaseGL();
	}

	public static void unloadStatic(GL gl)
	{
		// Unload textures
		gl.glDeleteTextures(glTex.length, glTex, 0);
		logger.info("Unloaded blocks texture.");
		initStatic = false;
	}

//----------------------------------------------------------------------------------------------------------------------	

	final Zone zone;
	boolean quadsChanged = true;
	
	final int[] glVBO = new int[1];
	static final int ARRAY = 0;
	int numVertices = 0;

	final Set<Quad> quads = new HashSet<Quad>();

	// TODO Unload VBO during destruction or after switching to another mode
	// TODO Switching to impostor mode
	
	public ZoneRenderer(GL gl, Zone zone)
	{
		this.zone = zone;

		this.zone.addZoneListener(this);

		gl.glGenBuffers(glVBO.length, glVBO, 0);
	}
	
	public void render(GL gl)
	{
		if (!initStatic)
			return;

		// Update the data
		if (quadsChanged)
		{
	        refreshVBO(gl);
			
			quadsChanged = false;
		}

		// TODO Lighting
		// Render the VBO
		gl.glEnableClientState(GL.GL_VERTEX_ARRAY);
		gl.glEnableClientState(GL.GL_TEXTURE_COORD_ARRAY);
		gl.glEnableClientState(GL.GL_COLOR_ARRAY);
		
		gl.glBindTexture(GL.GL_TEXTURE_2D, glTex[0]);

		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, glVBO[ARRAY]);
		gl.glTexCoordPointer(2, GL.GL_FLOAT, 8 * BYTES, 3 * BYTES);
		gl.glColorPointer(3, GL.GL_FLOAT, 8 * BYTES, 5 * BYTES);
		gl.glVertexPointer(3, GL.GL_FLOAT, 8 * BYTES, 0 * BYTES);

		gl.glDrawArrays(GL.GL_QUADS, 0, numVertices);
		
		gl.glDisableClientState(GL.GL_VERTEX_ARRAY);
		gl.glDisableClientState(GL.GL_TEXTURE_COORD_ARRAY);
		gl.glDisableClientState(GL.GL_COLOR_ARRAY);

		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0);
		
		// TODO Render portal polygons to stencil buffer and use occlusion queries to determine whether they have enough pixels to be worth rendering
	}

	/* (non-Javadoc)
     * @see cyclopean.world.Zone.ZoneListener#onChange(cyclopean.world.Zone)
     */
    @Override
    public void onChange(Zone zone, Set<Integer> ids)
    {
    	if (zone == this.zone)
    	{
    		Iterator<Quad> iter = quads.iterator();
    		do
    		{
    			final Quad quad = iter.next();
    			if (ids.contains(quad.blockId))
    				iter.remove();
    		}
    		while (iter.hasNext());
    		
    		for (int i : ids)
    		{
        		final byte b = zone.blocks[i];
        		final int z = i % Zone.SIZE_Z;
        		final int x = (i / Zone.SIZE_Z) % Zone.SIZE_X;
        		final int y = (i / Zone.SIZE_Z) / Zone.SIZE_X;

        		if (BlockType.isVis[b])
        		{
        			// West
            		if ((x == 0) || (BlockType.isTrans[zone.blocks[i - Zone.PER_X]]))
            		{
            			quads.add(new Quad(Face.WEST, x, y, z, b));
            		}

        			// East
            		if ((x == Zone.SIZE_X - 1) || (BlockType.isTrans[zone.blocks[i + Zone.PER_X]]))
            		{
            			quads.add(new Quad(Face.EAST, x, y, z, b));
            		}
            		
        			// South
            		if ((y == 0) || (BlockType.isTrans[zone.blocks[i - Zone.PER_Y]]))
            		{
            			quads.add(new Quad(Face.SOUTH, x, y, z, b));
            		}
    
        			// North
            		if ((y == Zone.SIZE_Y - 1) || (BlockType.isTrans[zone.blocks[i + Zone.PER_Y]]))
            		{
            			quads.add(new Quad(Face.NORTH, x, y, z, b));
            		}
    
            		// Down
            		if ((z == 0) || (BlockType.isTrans[zone.blocks[i - 1]]))
            		{
            			quads.add(new Quad(Face.DOWN, x, y, z, b));
            		}
    
        			// Up
            		if ((z == Zone.SIZE_Z - 1) || (BlockType.isTrans[zone.blocks[i + 1]]))
            		{
            			quads.add(new Quad(Face.UP, x, y, z, b));
            		}
        		}

        		if (BlockType.isTrans[b])
    			{
        			byte o;
        			
        			// West
        			if (x != 0)
        			{
            			o = zone.blocks[i - Zone.PER_X];
                		if (BlockType.isVis[o])
                		{
                			quads.add(new Quad(Face.EAST, x-1, y, z, o));
                		}
        			}
        			
        			// East
        			if (x != Zone.SIZE_X - 1)
        			{
            			o = zone.blocks[i + Zone.PER_X];
                		if (BlockType.isVis[o])
                		{
                			quads.add(new Quad(Face.WEST, x+1, y, z, o));
                		}
        			}
        			
        			// TODO North, south, up, down
    			}
    		}
    		
    		quadsChanged = true;
    	}
    }
    
    @Override
    public void onInit(Zone zone)
    {
    	if (zone == this.zone)
    	{
    		createQuads();
        	logger.info("Initialized a zone renderer.");
    	}
    }

	/**
     * 
     */
    private void createQuads()
    {
    	// Generate polygons
    	quads.clear();

    	int i = -1;
    	for (int y = 0; y < Zone.SIZE_X; y++)
    	{
        	for (int x = 0; x < Zone.SIZE_X; x++)
        	{
            	for (int z = 0; z < Zone.SIZE_Z; z++)
            	{
            		i++;
            		
            		final byte b = zone.blocks[i];
            		
            		if (!BlockType.isVis[b])
            			continue;

        			// West
            		if ((x == 0) || (BlockType.isTrans[zone.blocks[i - Zone.PER_X]]))
            		{
            			quads.add(new Quad(Face.WEST, x, y, z, b));
            		}

        			// East
            		if ((x == Zone.SIZE_X - 1) || (BlockType.isTrans[zone.blocks[i + Zone.PER_X]]))
            		{
            			quads.add(new Quad(Face.EAST, x, y, z, b));
            		}
            		
        			// South
            		if ((y == 0) || (BlockType.isTrans[zone.blocks[i - Zone.PER_Y]]))
            		{
            			quads.add(new Quad(Face.SOUTH, x, y, z, b));
            		}

        			// North
            		if ((y == Zone.SIZE_Y - 1) || (BlockType.isTrans[zone.blocks[i + Zone.PER_Y]]))
            		{
            			quads.add(new Quad(Face.NORTH, x, y, z, b));
            		}

            		// Down
            		if ((z == 0) || (BlockType.isTrans[zone.blocks[i - 1]]))
            		{
            			quads.add(new Quad(Face.DOWN, x, y, z, b));
            		}

        			// Up
            		if ((z == Zone.SIZE_Z - 1) || (BlockType.isTrans[zone.blocks[i + 1]]))
            		{
            			quads.add(new Quad(Face.UP, x, y, z, b));
            		}
            	}
        	}
    	}
    	
    	quadsChanged = true;
    }
    
    private void refreshVBO(GL gl)
    {
		// Create vertex data arrays
		final FloatBuffer verts = BufferUtil.newFloatBuffer(quads.size() * 4 * 8);
		numVertices = quads.size() * 4;

		for (Quad q : quads)
		{
			verts.put(q.data);
		}

		verts.rewind();

		// TODO Use glBufferSubData when numVertices doesn't decrease enough
		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, glVBO[ARRAY]);
		gl.glBufferData(GL.GL_ARRAY_BUFFER, numVertices * 8 * BYTES, verts, GL.GL_DYNAMIC_DRAW);	// 8 = XYZUVRGB

		gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0);
    }
    
    private static enum Face
    {
    	EAST (1, 3, 7, 5, 4, 5, 8, 7, 0.85f),
    	NORTH(3, 2, 6, 7, 1, 2, 5, 4, 0.50f),
    	UP   (4, 5, 7, 6, 3, 4, 7, 6, 1.00f),
    	WEST (2, 0, 4, 6, 4, 5, 8, 7, 0.85f),
    	SOUTH(0, 1, 5, 4, 1, 2, 5, 4, 0.50f),
    	DOWN (3, 1, 0, 2, 0, 1, 4, 3, 0.25f);
    	
    	static final float[][] P = new float[][]
    	                           {{0, 0, 0},
                            		{1, 0, 0},
                            		{0, 1, 0},
                            		{1, 1, 0},
                            		{0, 0, 1},
                            		{1, 0, 1},
                            		{0, 1, 1},
                            		{1, 1, 1}};
    	
    	static final float[][] T = new float[][]
 	                               {{0, TEX_Y_STEP},
                             		{0.5f * TEX_X_STEP, TEX_Y_STEP},
                             		{TEX_X_STEP, TEX_Y_STEP},
                             		{0, 0.5f * TEX_Y_STEP},
                             		{0.5f * TEX_X_STEP, 0.5f * TEX_Y_STEP},
                             		{TEX_X_STEP, 0.5f * TEX_Y_STEP},
                             		{0, 0},
                             		{0.5f * TEX_X_STEP, 0},
                             		{TEX_X_STEP, 0}};

    	final int p0;
    	final int p1;
    	final int p2;
    	final int p3;
    	final int t0;
    	final int t1;
    	final int t2;
    	final int t3;
    	final float bright;
    	
    	private Face(int p0, int p1, int p2, int p3, int t0, int t1, int t2, int t3, float bright)
    	{
    		this.p0 = p0;
    		this.p1 = p1;
    		this.p2 = p2;
    		this.p3 = p3;
    		this.t0 = t0;
    		this.t1 = t1;
    		this.t2 = t2;
    		this.t3 = t3;
    		this.bright = bright;
    	}
    }
    
    private static class Quad
    {
    	final int hash;
    	final int blockId;
    	final float[] data = new float[32];	// 4 verts * (3 pos coord + 2 tex coord + 3 color)
    	
    	Quad(Face face, int x, int y, int z, byte blockType)
    	{
    		final float tx = (byte)(blockType % BlockType.X_COUNT) * TEX_X_STEP;
    		final float ty = (byte)(blockType / BlockType.X_COUNT) * TEX_Y_STEP;

    		data[ 0] = x + Face.P[face.p0][0];
    		data[ 1] = y + Face.P[face.p0][1];
    		data[ 2] = z + Face.P[face.p0][2];
    		data[ 3] = tx + Face.T[face.t0][0];
    		data[ 4] = ty + Face.T[face.t0][1];
    		data[ 5] = face.bright;
    		data[ 6] = face.bright;
    		data[ 7] = face.bright;

    		data[ 8] = x + Face.P[face.p1][0];
    		data[ 9] = y + Face.P[face.p1][1];
    		data[10] = z + Face.P[face.p1][2];
    		data[11] = tx + Face.T[face.t1][0];
    		data[12] = ty + Face.T[face.t1][1];
    		data[13] = face.bright;
    		data[14] = face.bright;
    		data[15] = face.bright;

    		data[16] = x + Face.P[face.p2][0];
    		data[17] = y + Face.P[face.p2][1];
    		data[18] = z + Face.P[face.p2][2];
    		data[19] = tx + Face.T[face.t2][0];
    		data[20] = ty + Face.T[face.t2][1];
    		data[21] = face.bright;
    		data[22] = face.bright;
    		data[23] = face.bright;

    		data[24] = x + Face.P[face.p3][0];
    		data[25] = y + Face.P[face.p3][1];
    		data[26] = z + Face.P[face.p3][2];
    		data[27] = tx + Face.T[face.t3][0];
    		data[28] = ty + Face.T[face.t3][1];
    		data[29] = face.bright;
    		data[30] = face.bright;
    		data[31] = face.bright;
    		
    		blockId = z + (Zone.PER_X * x) + (Zone.PER_Y * y);
    		hash = blockId * 6 + face.ordinal();
    	}

		/* (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj)
        {
	        return (obj != null) && (obj instanceof Quad) && (hash == ((Quad)obj).hash);
        }

		/* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode()
        {
	        return hash;
        }
    }
    
    public void unload(GL gl)
    {
    	gl.glDeleteBuffers(glVBO.length, glVBO, 0);
    	glVBO[0] = 0;
    	logger.info("Unloaded a zone's vertex buffer object.");
    }

	/* (non-Javadoc)
     * @see java.lang.Object#finalize()
     */
    @Override
    protected void finalize() throws Throwable
    {
	    if (glVBO[0] != 0)
	    {
	    	final GL gl = Game.get().grabGL();
	    	unload(gl);
	    	Game.get().releaseGL();
	    }

	    super.finalize();
    }
}
